﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Reflection;
using System.Collections;
using GameLib.Serialization;
using GameLib.Utils;

namespace Sandbox.Serialization.Serializers
{
	public class ObjectSerializer : XmlSerializer
	{
		protected override object DeserializeInner(object obj, Type type, XmlReader reader, DataFlags dataFlags)
		{
			if (IsDictionary(type))
			{
				return DeserializeDictionary(obj, type, reader, dataFlags);
			}
			else if (IsCollection(type))
			{
				return DeserializeCollection(obj, type, reader, dataFlags);
			}
			else
			{
				return DeserializeProperties(obj, type, reader, dataFlags);
			}
		}

		protected virtual object DeserializeDictionary(object obj, Type type, XmlReader reader, DataFlags dataFlags)
		{
			IDictionary dict = obj as IDictionary;

			if (dict == null)
				throw new Exception();

			// grab the IDicitonary<> interface
			Type dictType = type.GetInterface("IDictionary`2");

			Type keyType = typeof(object);
			Type valueType = typeof(object);

			if (dictType != null)
			{
				Type[] keyValueTypes = dictType.GetGenericArguments();
				keyType = keyValueTypes[0];
				valueType = keyValueTypes[1];
			}

			do
			{
				if (reader.IsStartElement())
				{
					// read sub object

					Type thisValueType = GetElementType(reader.Name, valueType);

					string keyName = reader.GetAttribute(KeyWord_Key);

					if (keyName == null)
						throw new Exception("Dictionary elements must have 'Key' attribute");

					// deserialize the key
					// TODO: handle special key data flags
					object key = ConvertFromString(null, keyType, keyName, DataFlags.None);

					// instance the object
					// TODO: handle special value data flags
					object value = DeserializeSubObject(null, thisValueType, reader, DataFlags.None);

					// TODO: could value be null here?

					dict[key] = value;
				}
				else
				{
					reader.Read();
				}

				reader.MoveToContent();

			} while (reader.NodeType != XmlNodeType.EndElement && reader.NodeType != XmlNodeType.None);

			return obj;
		}

		protected virtual object DeserializeCollection(object obj, Type type, XmlReader reader, DataFlags dataFlags)
		{
			Type itemType = typeof(object);

			// grab the IDicitonary<> interface
			Type collectionType = type.IsInterface ? type : type.GetInterface("ICollection`1");

			if (collectionType != null)
			{
				Type[] itemTypes = collectionType.GetGenericArguments();
				itemType = itemTypes[0];
			}

			Type[] mtsAdd = { itemType };
			MethodInfo miAdd = obj.GetType().GetMethod("Add", mtsAdd);

			do
			{
				if (reader.IsStartElement())
				{
					// read sub object

					// attempt to use the type indicated by the element name
					Type thisItemType = GetElementType(reader.Name, itemType);

					// Deserialize this item
					// TODO: handle item data flags
					object item = DeserializeSubObject(null, thisItemType, reader, DataFlags.None);

					// TODO: could item be null here?

					object[] parms = { item };
					miAdd.Invoke(obj, parms);
				}
				else
				{
					reader.Read();
				}

				reader.MoveToContent();

			} while (reader.NodeType != XmlNodeType.EndElement && reader.NodeType != XmlNodeType.None);

			return obj;
		}

		/*
		protected virtual object DeserializeProperties(object obj, Type type, XmlReader reader, DataFlags dataFlags)
		{
			do
			{
				if (reader.NodeType != XmlNodeType.Element)
				{
					throw new Exception();
				}

				string propName = reader.Name;

				PropertyInfo pi = type.GetProperty(propName, BindingFlags.Instance|BindingFlags.Public|BindingFlags.NonPublic);

				if (pi == null)
				{
					throw new Exception();
				}

				Type propType = pi.PropertyType;

				// see if a sub type is indicated

				Type instType = propType;

				string instTypeName = reader.GetAttribute(KeyWord_Type);

				if (instTypeName != null)
				{
					instType = TypeNameTable.GetType(instTypeName);

					if (!instType.IsSubclassOf(propType))
						throw new Exception(string.Format("Type {0} is not a sub class of Type {1} cannot be used as sub object type!", instType, propType));
				}

				// data flags for this property
				DataFlags propDataFlags = GetPropertyDataFlags(pi);

				// see if we need to pass over the existing property value
				object curValue = null;

				// not value type, not reference, instance type same as property type
				if (!propType.IsValueType &&
					(propDataFlags & DataFlags.Reference) != DataFlags.Reference &&
					instType == propType)
				{
					curValue = pi.GetValue(obj, null);
				}

				object newValue = DeserializeSubObject(curValue, instType, reader, propDataFlags);

				// set new value if necessary
				if (curValue == null || newValue != curValue)
				{
					pi.SetValue(obj, curValue, null);
				}

				// now it should be at the node after the object end tag
				reader.MoveToContent();

			} while (reader.NodeType != XmlNodeType.EndElement && reader.NodeType != XmlNodeType.None);

			return obj;
		}*/

		protected override void SerializeInner(object obj, List<PropertyInfo> props, XmlWriter writer, DataFlags dataFlags)
		{
			Type type = obj.GetType();
			if (IsDictionary(type))
			{
				SerializeDictionary(obj, props, writer, dataFlags);
			}
			else if (IsCollection(type))
			{
				SerializeCollection(obj, props, writer, dataFlags);
			}
			else
			{
				SerializeProperties(obj, props, writer, dataFlags);
			}
		}

		protected virtual void SerializeDictionary(object obj, List<PropertyInfo> props, XmlWriter writer, DataFlags dataFlags)
		{
			throw new NotImplementedException();
		}

		protected virtual void SerializeCollection(object obj, List<PropertyInfo> props, XmlWriter writer, DataFlags dataFlags)
		{
			IEnumerable collection = obj as IEnumerable;

			if (collection == null)
				throw new Exception(string.Format("Somehow {0} is identified as collection but not an enumerable", obj));

			foreach (object item in collection)
			{
				if (item != null)
					SerializeSubObject(item, item.GetType(), item.GetType().Name, writer, DataFlags.None);
			}
		}

		/*
		protected virtual void SerializeProperties(object obj, List<PropertyInfo> props, XmlWriter writer, DataFlags dataFlags)
		{
			foreach (PropertyInfo pi in props)
			{
				object elemValue = pi.GetValue(obj, null);
				Type elemType = elemValue != null ? elemValue.GetType() : pi.PropertyType;

				// TODO: grab data flags from pi's attribute
				// TODO: grab alternative prop name?
				SerializeSubObject(elemValue, elemType, pi.Name, writer, DataFlags.None);
			}
		}*/

		public override Type SupportedType
		{
			get { return typeof(object); }
		}

		private bool IsCollection(Type type)
		{
			if (type.IsInterface)
				return type.GetGenericTypeDefinition() == typeof(ICollection<>);

			return type.GetInterface("ICollection`1") != null;
		}

		private bool IsDictionary(Type type)
		{
			return typeof(IDictionary).IsAssignableFrom(type);
		}

		private Type GetElementType(string elemName, Type defType)
		{
			if (elemName == defType.Name)
				return defType;

			Type elemType = TypeNameTable.GetType(elemName);

			if (!defType.IsAssignableFrom(elemType))
				throw new Exception(string.Format("Type {0} is not a sub class of nor the same Type {1} cannot be used as element type!", elemType, defType));
			
			return elemType;
		}
	}
}
